首页 > 其他分享 >SPI驱动-基于ICM20608六轴MEMS传感器

SPI驱动-基于ICM20608六轴MEMS传感器

时间:2024-07-01 22:53:30浏览次数:19  
标签:include SPI 六轴 dev spi MEMS icm20608 data

1 IMX6ULL的SPI控制器简介

SPI是全双工同步串行通讯总线,是一个四线结构的总线协议,其使用比IIC简洁许多,具体关于SPI总线协议的内容可以自行查阅资料。
1.1 SPI控制器介绍

imx6ull中有四个ECSPI控制器,也即四个增强型SPI控制器,也可以当作普通的SPI控制器使用。这里又要和51单片机做区分:对于51单片机,它没有SPI控制器,只能够通过通用引脚来软件模拟SPI通信,但是对于有SPI控制器的cpu来说,我们通过寄存器配置好SPI控制器后,只需要将数据写入发送寄存器或者通过设置寄存器XCH位触发SPI传输,控制器就会自动地在时钟的同步下进行SPI的数据收发操作,而不需要CPU的干预,从而减轻了CPU的负担,在SPI传输期间CPU可以去执行别的代码。
需要注意的是,SPI是同步串行通信总线,因此它的收和发是同步进行的,发送出去一位的同时,主机也会通过MISO总线收到一位数据,所以SPI的传输大多数情况下使用写入触发。

上图是imx6ull的SPI控制器的内部框图,总线下来一行就是对应的控制器的寄存器,有TXDATA(发送寄存器),RXDATA(接收寄存器),STATREG(状态寄存器),INTREG(中断寄存器),DMAREG,PERIODREG,TESTREG,CONREG。


主模式下的SPI工作流程如下:

IMX6ULL的四个SPI控制器的用法和寄存器都是类似的,以SPI3为例进行说明,控制器相关的寄存器如下

上面画红色方框的寄存器就是大多数情况下会使用到的寄存器,关于这些寄存器的意义用法,其实根据名字也可以猜出大概:RXDATA,RX一般表示接收,因此RXDATA可能是接收数据寄存器,TXDATA就是数据发送寄存器,CONREG应该是configure register的缩写,所以可能是配置寄存器,COONFIGREG可能也是配置寄存器,INTREG应该是interrupt register的缩写,应该是跟中断相关的寄存器,DMAREG可能是跟DMA数据运输相关的寄存器,STATREG是state register缩写,可能是记录SPI控制器状态的寄存器,PERIODREG是period register,周期寄存器,那可能是跟SPI控制器时钟频率相关的寄存器,TESTREG可能是和控制器的测试相关的寄存器,MSGDATA猜不出来。关于寄存器的用法和详细介绍,可以参考芯片参考手册的SPI章节,在很多开发板教程中也有罗列出来,下面的内容来自韦东山逻辑教程的整理:

这里再补充一下有关SPI控制器的时钟部分。


从图中可以看出,SPI控制器输入的时钟源是ECSPI_CLK_ROOT,PPL3经过8分频后得到60Mhz,然后ECSPI_CLK_PODF使用默认值不分频,因此输入ECSPI控制器的时钟源频率是60MHZ。需要注意的是,在SPI控制器内部,还可以进行进一步的分配,具体可以参考上面的寄存器的介绍。

1.2 SPI控制器裸机接口代码
在SPI编程中,也是遵循了和IIC类似的上下分层的原则,要根据CPU主控芯片实际的SPI控制器特性编写SPI接口程序,然后SPI从设备调用这个SPI接口程序中的函数去编写跟从设备硬件特性符合的SPI程序。实际上,对于主机而言,SPI控制器就是负责收发数据,至于发送出去的数据用来做什么由从设备的设计规则来决定,同时主机接收到的从设备数据用作什么,也是根据从设备的特性在从设备的SPI程序中表现出来。对于SPI控制器机制的理解,从裸机程序入手分析更加直观。下面的程序来自于韦东山的裸机SPI控制器程序。

spi.h代码

点击查看代码
#ifndef _SPI_H_
#define _SPI_H_


typedef struct 
{
	volatile unsigned int RXDATA;
	volatile unsigned int TXDATA;
	volatile unsigned int CONREG;
	volatile unsigned int CONFIGREG;
	volatile unsigned int INTREG;
	volatile unsigned int DMAREG;
	volatile unsigned int STATREG;
	volatile unsigned int PERIODREG;
	volatile unsigned int TESTREG;   
	volatile unsigned int RESERVED[0x20];
	volatile unsigned int MSGATA;
	
	
}SPI_CTRL;
		  			 		  						  					  				 	   		  	  	 	  

#define ESCPI1_BASE (0x2008000)
#define ESCPI2_BASE (0x200c000)
#define ESCPI3_BASE (0x2010000)
#define ESCPI4_BASE (0x2014000)

		  			 		  						  					  				 	   		  	  	 	  
unsigned char  spi_init(SPI_CTRL * spi_num);
void spi_select(void);
void spi_deselect(void);
unsigned char spi_writeread(SPI_CTRL *spi_num,unsigned char uc_txdata);
unsigned char spi_test_rw(SPI_CTRL *spi_num);
#endif

spi.c代码

点击查看代码
#include "spi.h"
#include "my_printf.h"

/*spi3对应的iomux基址*/
#define UART2_TX_BASE	0x20e0094
#define UART2_RX_BASE	0x20e0098
#define UART2_CTS_BASE	0x20e009C
#define UART2_RTS_BASE	0x20e00A0

static volatile unsigned int *GPIO1_GDIR                             ;
static volatile unsigned int *GPIO1_DR                               ;
/**********************************************************************
	 * 函数名称: iomuxc_sw_set
	 * 功能描述: 多功能引脚IO模式设置
	 * 输入参数:@base :要设置的引脚基址
	 			@uc_mode:引脚要设置的模式,值为0/1/2/3/4/5/6/7/8,具体查询手册确定
	 * 输出参数:无
	 * 返 回 值: 无
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/02/20		 V1.0	  芯晓		  创建
 ***********************************************************************/

static void iomuxc_sw_set(unsigned int base,unsigned int uc_mode)
{
	*((volatile unsigned int *)base)  = uc_mode&0x000f;
}
/**********************************************************************
 * 函数名称: spi_init
 * 功能描述: spi初始化,包括引脚等 默认设置频率为1M,,,,,
 * 输入参数: @uc_num :要控制的spi接口的基址
 * 输出参数:初始化结果
 * 返 回 值:  返回0,表示函数正常返回
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 *  2020/02/20		 V1.0	  xy(xinyang)		  创建
 ***********************************************************************/
unsigned char  spi_init(SPI_CTRL *uc_num)
{
	/*
		1、清除CONREG寄存器的EN位 来复位模块
		2、在ccm中使能spi时钟
		3、配置control register,然后设置CONREG的EN位来使spi模块退出复位
		4、配置spi对应的IOMUX引脚
		5、根据外部spi设备规格来合适的配置spi寄存器
		
	*/
		printf("spi 初始化开始\n\r") ; 

	/**/
	uc_num->CONREG =  0;// clear all bits
	/*
		bit0:使能SPI
		bit3:写入TXDATA之后,立即发送
		bit4:设置通道0为master mode
		bit31:20 设置burst length ,7表示为8bits,一个字节
	*/
	uc_num->CONREG |= (7<<20)|(1<<4)|(1<<3)|(1<<0);
	/*	CONFIGREG采用默认设置
		*
		*bit0 		PHA=0
		*bit7:4 	sclk高电平有效
		*bit11:8	通道片选信号,当SMC =1的时候,无效(当前处于SMC=1模式)
		*bit15:12	POL=0
		*bit19:16	数据线空闲为高电平
		*bit23:20	sclk空闲为低电平
		*bit28:24	设置消息长度 ,该产品不进行使用
		*
	*/
	uc_num->CONFIGREG = 0;//	
	/*设置时钟相关的*/
	/*  
	从RM手册chapter18中,我们得知时钟来源为PLL3
	1、pll3_sw_clk_sel为0,则选择pll3;为1则选择ccm_pll3_bys,时钟   默认选择pll3 。输出pll3_sw_clk给spi进行使用  输出给spi的时钟为480M/8=60Mhz
	2、我们需要使能spi的时钟进行使用,通过CCM_CCGR1的bit5:2来进行设置 这部分在制作.imx文件的时候初始化,可以不处理
	3、计算时钟频率 CONREG寄存器
		bit15:12 div_1 
		bit11:8	div_2
	最终提供给spip的时钟为
	60M/(div+1)*(2^div_2))
	假设我们要使用的时钟是4M
	则我们设置bit15:12 = 15即可  60M/4 = 15Mhz	
	*/
	uc_num->CONREG &= ~(0xf<<12|0xf<<8);//清除原先的时钟频率设置
	uc_num->CONREG |= (14<<12); //设置clk = 60/(14+1) = 4M
	printf("spi 初始化结束\n\r"); 
	//引脚初始化
	iomuxc_sw_set(UART2_TX_BASE,5);//设置为GPIO作为片选来进行使用。GPIO1_IO20
	GPIO1_GDIR  = (volatile unsigned int *)(0x209C000 + 0x4);
	GPIO1_DR  = (volatile unsigned int *)(0x209C000);
	*GPIO1_GDIR |= (1<<20);//设置为输出
	*GPIO1_DR |= (1<<20);	
//	iomuxc_sw_set(UART2_TX_BASE,8);
	iomuxc_sw_set(UART2_RX_BASE,8);
	iomuxc_sw_set(UART2_RTS_BASE,8);
	iomuxc_sw_set(UART2_CTS_BASE,8);
	return 0;
}
/**********************************************************************
 * 函数名称: spi_select
 * 功能描述: spi片选拉低,GPIO来实现,
 * 输入参数: 无
 * 输出参数:无
 * 返 回 值: 
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 *  2020/02/20		 V1.0	  xy(xinyang)		  创建
 ***********************************************************************/
void spi_select(void)
{
	*GPIO1_DR &= ~(1<<20);
}
/**********************************************************************
 * 函数名称: spi_select
 * 功能描述: spi片选拉高,GPIO来实现,
 * 输入参数: 无
 * 输出参数:无
 * 返 回 值: 
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 *  2020/02/20		 V1.0	  xy(xinyang)		  创建
 ***********************************************************************/
void spi_deselect(void)
{
	*GPIO1_DR |= (1<<20);
}
/**********************************************************************
 * 函数名称: spi_writeread
 * 功能描述: spi输入和输出数据
 * 输入参数: @SPI_CTRL SPI控制器基址
 			@uc_txdata 要发送的数据
 * 输出参数:读出的数据
 * 返 回 值: 
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 *  2020/02/20		 V1.0	  xy(xinyang)		  创建
 ***********************************************************************/
unsigned char spi_writeread(SPI_CTRL *spi_num,unsigned char uc_txdata)
{
	/*片选型号*/
	spi_num->CONREG &= ~(3<<18);
	spi_num->CONREG |= 0<<18 ;
	
	while(!(spi_num->STATREG&(1<<0)));//如果FIFO时空的话,则填充数据以开始下一次发送
	spi_num->TXDATA = uc_txdata;

	while(!(spi_num->STATREG&(1<<3)));//等待接收数据完成,当为1的时候表示有接收数据存在,可以进行读取
	return spi_num->RXDATA;
}
/**********************************************************************
 * 函数名称: spi_test_rw
 * 功能描述: spi输入和输出数据
 * 输入参数: @SPI_CTRL SPI控制器基址
 * 输出参数: 回环自测的结果
 * 返 回 值: 成功则返回0,否则返回-1
 * 修改日期        版本号     修改人	      修改内容
 * -----------------------------------------------
 *  2020/02/20		 V1.0	  xy(xinyang)		  创建
 ***********************************************************************/
unsigned char spi_test_rw(SPI_CTRL *spi_num)
{
	/*
		*enable loopback test
		*transmitter and receiver sections internally connected for loopback
	*/
	unsigned char uc_cnt=0;
	unsigned char uc_buf_write[20]={0};
	unsigned char uc_buf_read[20]={0};
	//设置进入loop模式,进行测试
	spi_num->TESTREG = (1<<31);
	printf("spi进入回环测试模式\n\r");	 
	//造数
	for(uc_cnt=0;uc_cnt<20;uc_cnt++)
	{
		uc_buf_write[uc_cnt] = 0x20+uc_cnt;
	}
	//进行读写测试
	for(uc_cnt=0;uc_cnt<20;uc_cnt++)
	{
		printf("write_cnt %d\t",uc_cnt);	
		uc_buf_read[uc_cnt]=spi_writeread(spi_num,uc_buf_write[uc_cnt]);
		printf("write %d\t",uc_buf_write[uc_cnt]);	
		printf("read %d\n\r",uc_buf_read[uc_cnt]);	
	}
	//进行数据对比
	for(uc_cnt=0;uc_cnt<20;uc_cnt++)
	{
		if(uc_buf_read[uc_cnt]!=uc_buf_write[uc_cnt])
		{/*表示回环测试失败,存在问题*/
			printf("!!!spi回环测试失败\n\r");			
			return -1;
		}
	}
	printf("@@@spi回环测试成功\n\r");
	printf("spi退出回环测试模式\n\r");
		//exit loopback mode
	spi_num->TESTREG = 0;
	return 0;
	
}

在上述代码中,printf用于串口打印输出,这部分有专门的裸机程序来负责实现,可不用纠结,这里我们只关注SPI控制器的代码框架和实现。下面具体解释每个函数。

在spi.h中通过一个结构体列出了SPI控制器相关的寄存器,结合占位字节,里面每个和寄存器名字对应的变量的相对地址(相对于结构体变量首地址)刚好和实际的控制器中各个寄存器的相对地址对应上,因此只要定义一个结构体指针,并把这个指针指向某个SPI控制器的寄存器组的第一个寄存器的地址,那么就可以方便的通过这个结构体指针访问控制器中的寄存器。

在上述的SPI控制器代码中,实现了SPI的复位,引脚功能复用设置,触发方式设置,时钟频率设置,时钟相位和数据极性设置等内容,可以多学习这种代码编写风格,非常清晰直观。

2 ICM-20608六轴MEMS传感器介绍
2.1 ICM-20608传感器使用方法介绍
ICM-20608是一个六轴姿态传感器,在无人机,平衡车上都有应用。ICM-20608的结构框图如下:

ICM-20608一些常用的寄存器如下表

完整的ICM20608的寄存器表如下:

使用ICM20608C传感器的SPI时序如下:
读时序:
(1)拉低片选信号
(2)发送寄存器地址,低七位表示寄存器地址,第7位表示对ICM-20608读还是写,这里1是读,0是写,这里是读,因此写1
(3)因为在上一部分的SPI驱动程序中设置了写入触发,因此向SPI控制器的TXDATA寄存器写入一个字节,就可以触发SPI传输同步读取到寄存器的值;
要继续读取下一个寄存器的值时,可以重复(1)~(3)的操作

写时序:
(1)拉低片选信号
(2)发送寄存器地址,低七位表示寄存器地址,第7位表示对ICM-20608读还是写,这里1是读,0是写,这里是写,因此写0
(3)向SPI控制器的TXDATA寄存器写入一个字节,SPI控制器就会自动传输
要继续写下一个寄存器的值时,可以重复(1)~(3)的操作

2.2 ICM-20608传感器的裸机代码
同样的,看裸机代码才能更直观的感受设备到底是如何工作的,下面的ICM-20608裸机代码来自韦东山老师的裸机代码,通过调用第一部分中SPI控制器的裸机代码程序中的函数接口来实现ICM-20608传感器的SPI时序。

ICM-20608的裸机代码如下:

点击查看代码
#include "spi.h"
#include "icm20608g.h"
#include "my_printf.h"
static ICM20608G_GYRO_ACC_adc icm20608g_get;
/**********************************************************************
	 * 函数名称: icm20608g_write_addr
	 * 功能描述: icm20608G向特定地址写入数据
	 * 输入参数:@uc_addr :要写入的地址
	 			@uc_data:要写入的数据
	 * 输出参数:无
	 * 返 回 值: 无
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
void icm20608g_write_addr(unsigned char uc_addr,unsigned char uc_data)
{
	unsigned char uc_read=0;
	uc_addr &= ~0x80;/*地址最高位为0表示写入*/
	spi_select();
	spi_writeread(ESCPI3_BASE,uc_addr);
	spi_writeread(ESCPI3_BASE,uc_data);
	spi_deselect();
}		  			 		  						  					  				 	   		  	  	 	  
/**********************************************************************
	 * 函数名称: icm20608g_read_addr
	 * 功能描述: icm20608G从特定地址读出数据
	 * 输入参数:@uc_addr :要读取的地址
	 * 输出参数:读出的数据
	 * 返 回 值:读出的数据
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char  icm20608g_read_addr(unsigned char uc_addr)
{
	unsigned char uc_read = 0;
	uc_addr |= 0x80;/*地址最高位为1表示读取*/	
	spi_select();
	spi_writeread(ESCPI3_BASE,uc_addr);
	uc_read=spi_writeread(ESCPI3_BASE,0xff);
	spi_deselect();
	return uc_read;
}
/**********************************************************************
	 * 函数名称: icm20608g_init
	 * 功能描述: icm20608G的初始化
	 * 输入参数:无
	 * 输出参数: 初始化的结果
	 * 返 回 值: 成功则返回0,否则返回-1
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char icm20608g_init(void)
{
	unsigned char uc_dev_id = 0;
	spi_init(ESCPI3_BASE);
	printf("icm20608g 初始化开始\n\r");
//	icm20608_write_addr(ICM20608G_PWR_MGMT_1,0x80);//设备复位
	icm20608g_write_addr(ICM20608G_PWR_MGMT_1,0x01);//设备退出复位
	//读取设备id并对比,如果不等于0xaf,则退出初始化
	uc_dev_id = icm20608g_read_addr(ICM20608G_WHO_AM_I);
	printf("read icm20608g id is 0x%x\n\r",uc_dev_id);
	if(uc_dev_id!=0xAF)
	{
		printf("read id fail\n\r");
		return -1;
	}
	icm20608g_write_addr(ICM20608G_SMPLRT_DIV,0x00);//采样率默认1K
	icm20608g_write_addr(ICM20608G_CONFIG, 0x00);//禁止FIFO
	icm20608g_write_addr(ICM20608G_GYRO_CONFIG,0x00);//使用默认量程和低通滤波器
	icm20608g_write_addr(ICM20608G_ACC_CONFIG,0x00);//使用默认量程
	icm20608g_write_addr(ICM20608G_ACC_CONFIG2,0x00);//使用默认低通滤波器
	icm20608g_write_addr(ICM20608G_LP_MODE_CFG,0x00);//关闭低功耗模式
	icm20608g_write_addr(ICM20608G_FIFO_EN,0x00);//禁止传感器FIFO
	icm20608g_write_addr(ICM20608G_PWR_MGMT_2,0x00);//使能传感器
	printf("icm20608g 初始化结束\n\r");
	return 0;
}
		  			 		  						  					  				 	   		  	  	 	  
/**********************************************************************
	 * 函数名称: icm20608g_read_len
	 * 功能描述: icm20608G从特定地址读取一定长度的数据,然后保存到指定地址
	 * 输入参数:@uc_addr:要读取的地址
	 			@buf :读取数据的缓存地址
	 			@uc_len:要读取数据的长度
	 * 输出参数: 读取结果
	 * 返 回 值: 成功则返回0
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char  icm20608g_read_len(unsigned char uc_addr,unsigned char *buf,unsigned char uc_len)
{
	unsigned char uc_cnt;
	uc_addr |= 0x80;/*地址最高位为1表示读取*/
	spi_select();
	spi_writeread(ESCPI3_BASE,uc_addr);
	for(uc_cnt=0;uc_cnt<uc_len;uc_cnt++)
	{
		buf[uc_cnt]=spi_writeread(ESCPI3_BASE,0xff);	//写入触发SPI,此时SPI控制器会控制SPI总线发出一个字节数据同时接收一个字节数据
	}
	spi_deselect();
	return 0;
}
/**********************************************************************
	 * 函数名称: print_x
	 * 功能描述: 将一定数量的数据通过串口进行打印
	 * 输入参数:@uc_buf:打印数据的缓存地址
	 			@uc_len :要打印数据的长度
	 * 输出参数: 无
	 * 返 回 值: 
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/

void print_x(unsigned char *uc_buf,unsigned char uc_len)
{
	unsigned char uc_cnt;
	for(uc_cnt=0;uc_cnt<uc_len;uc_cnt++)
	{
		printf("read %d : %x \n\r",uc_cnt,uc_buf[uc_cnt]);
	}	
}
/**********************************************************************
	 * 函数名称: icm20608g_read_acc
	 * 功能描述: 读取加速度原始数据信息
	 * 输入参数:无
	 * 输出参数: 读取结果
	 * 返 回 值: 成功则返回0 
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char  icm20608g_read_acc(void)
{
	unsigned char uc_buf[6];
	icm20608g_read_len(0x3b,uc_buf,6);
	icm20608g_get.acc_x_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);
	icm20608g_get.acc_y_adc = (signed short)((uc_buf[2]<<8)|uc_buf[3]);
	icm20608g_get.acc_z_adc = (signed short)((uc_buf[4]<<8)|uc_buf[5]);
	printf("@@加速度icm20608g_read_acc \n\r");
	print_x(uc_buf,6);
	return 0;
}
/**********************************************************************
	 * 函数名称: icm20608g_read_gyro
	 * 功能描述: 读取角速度原始数据信息
	 * 输入参数:无
	 * 输出参数: 读取结果
	 * 返 回 值: 成功则返回0 
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char  icm20608g_read_gyro(void)
{
	unsigned char uc_buf[6];
	icm20608g_read_len(0x43,uc_buf,6);
	icm20608g_get.gyro_x_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);
	icm20608g_get.gyro_y_adc = (signed short)((uc_buf[2]<<8)|uc_buf[3]);
	icm20608g_get.gyro_z_adc = (signed short)((uc_buf[4]<<8)|uc_buf[5]);
	printf("###角速度icm20608g_read_gyro \n\r");
	print_x(uc_buf,6);
	return 0;
}
		  			 		  						  					  				 	   		  	  	 	  
/**********************************************************************
	 * 函数名称: icm20608g_read_temp
	 * 功能描述: 读取温度原始数据信息
	 * 输入参数:无
	 * 输出参数: 读取结果
	 * 返 回 值: 成功则返回0 
	 * 修改日期 	   版本号	 修改人		  修改内容
	 * -----------------------------------------------
	 * 2020/03/04		 V1.0	  芯晓		  创建
 ***********************************************************************/
unsigned char  icm20608g_read_temp(void)
{
	unsigned char uc_buf[2];
	icm20608g_read_len(0x41,uc_buf,2);
	icm20608g_get.temp_adc = (signed short)((uc_buf[0]<<8)|uc_buf[1]);
	printf("$$$温度icm20608g_read_temp \n\r");
	print_x(uc_buf,2);
	return 0;
}





main.c的代码如下:

点击查看代码
#include "common.h"
#include "uart.h"
#include "my_printf.h"
#include "spi.h"
void delay(volatile unsigned int d)
{
	while(d--);
}
void system_init()
{
	
	boot_clk_gate_init();
	boot_clk_init();
	uart1_init();
	puts("init ok\r\n");
	
}
int  main()
{	
	unsigned char uc_cnt;
	icm20608g_init();//初始化传感器ICM-20608-G	
	for(uc_cnt=0;uc_cnt<1;uc_cnt++)
	{
		icm20608g_read_acc();
		icm20608g_read_gyro();
		icm20608g_read_temp();
	}
	return 0;
}


在main函数中会最终调用icm20608g_init函数来进行icm-20608的初始化,在这个函数里其实就是调用了各部分的函数来实现。
分析icm20608.c中的代码,在icm20608g_init函数中就是调用spi初始化函数,然后使用icm20608g_write_addr和icm20608g_read_addr函数进行ICM-20608的一系列初始化和检验,这两个函数内部其实就是调用了spi的读写函数来最终实现的。同样的在main函数中读取加速度、温度、陀螺仪数据的函数最终也是通过spi的读写函数来实现。因此这里其实体现了分层和面向对象的编程思想。对于SPI控制器,对外提供统一的接口,然后icm-20608的SPI程序就调用这些接口来实现传感器的操作。好的代码值得反复品味。
需要注意的是,上述裸机代码中读取传感器数据时只是读取了ADC值,并没有转换为真实值,因此要得到真实的值还需要进行进一步的转换

3 Linux下的SPI驱动程序

3.1 SPI子系统中常用的函数
(1)spi_sync_transfer函数:用于执行一次完整的SPI传输操作,可以一次传输多个数据块,在传输完成后将接收到的数据存储在struct spi_transfer结构体中,它是同步调用,在传输完成之前会一直等待。
示例代码如下:

点击查看代码
#include <linux/spi/spi.h>

struct spi_device *spi_dev;  // SPI设备实例

// 创建spi_transfer结构体数组,用于描述传输数据
struct spi_transfer transfers[2];
u8 tx_data[2] = {0x55, 0xAA};  // 发送的数据
u8 rx_data[2];  // 接收的数据

// 初始化spi_transfer结构体
memset(transfers, 0, sizeof(transfers));

transfers[0].tx_buf = tx_data;
transfers[0].rx_buf = rx_data;
transfers[0].len = sizeof(tx_data);

// 执行SPI传输
spi_sync_transfer(spi_dev, transfers, ARRAY_SIZE(transfers));

// 处理接收到的数据
// ...

(2)spi_sync函数:它是对spi_sync_transfer的封装,用于简化SPI传输操作
示例代码如下:

点击查看代码
#include <linux/spi/spi.h>

struct spi_device *spi_dev;  // SPI设备实例

// 创建spi_message结构体,用于描述传输序列
struct spi_message msg;
struct spi_transfer transfer1, transfer2;
u8 tx_data1[2] = {0x55, 0xAA};  // 第一个传输的发送数据
u8 tx_data2[2] = {0xBB, 0xCC};  // 第二个传输的发送数据
u8 rx_data1[2];  // 第一个传输的接收数据
u8 rx_data2[2];  // 第二个传输的接收数据

// 初始化spi_message结构体和spi_transfer结构体
spi_message_init(&msg);
memset(&transfer1, 0, sizeof(transfer1));
memset(&transfer2, 0, sizeof(transfer2));

// 设置第一个传输的spi_transfer结构体
transfer1.tx_buf = tx_data1;
transfer1.rx_buf = rx_data1;
transfer1.len = sizeof(tx_data1);

// 添加第一个传输到spi_message
spi_message_add_tail(&transfer1, &msg);

// 设置第二个传输的spi_transfer结构体
transfer2.tx_buf = tx_data2;
transfer2.rx_buf = rx_data2;
transfer2.len = sizeof(tx_data2);

// 添加第二个传输到spi_message
spi_message_add_tail(&transfer2, &msg);

// 执行SPI传输
spi_sync(spi_dev, &msg);

// 处理接收到的数据
// ...
spi_sync_transfer是直接控制SPI传输的函数,需要手动创建和管理struct spi_transfer结构体,适用于需要更精确控制传输的情况。而spi_sync是对传输过程进行封装的函数,通过struct spi_message来描述传输序列,更加方便和简化,适用于大部分常见的SPI传输场景。无论哪个函数,**本质都是将数据写入SPI控制器的发送寄存器来触发SPI的传输,因此发多少个字节就可以读到多少个字节。** 对比IIC子系统的使用,SPI子系统中spi_sync_transfer的使用其实和i2c_transfer函数的使用是很类似的。

3.2 ICM-20608的SPI驱动程序
ICM20608有SPI接口和IIC接口,在正点原子ixm6ull开发板上使用的是SPI接口,其硬件原理图如下所示:


从原理图可以看到,开发板上的ICM-20608传感器使用了imx6ull芯片上的UART2_TXD、UART2_RXD、UART2_RTS、UART2_CTS四个引脚,这四个引脚分别可以复用为ECSPI3_SS0,ECSPI3_SCLK,ECSPI3_MISO、ECSPI3_MOSI。查看ixm6ull芯片参考手册,在IOMUX复用控制器章节可以查找到IO复用控制器中这个四个引脚的复用寄存器。

从上面的四个复用寄存器可以看到,上述四个引脚可以复用为一组SPI接口。

继续看上面的原理图,我们可以看到,虽然在开发板上icm20608是被接到了ixm6ull芯片中的spi3控制器的接口引脚,但是实际上在开发板中引出了它的IO,如下图所示:


这是开发板的底板原理图中引出IO部分的原理图,可以看到UART2_TXD、UART2_RXD、UART2_RTS、UART2_CTS这四个引脚都是引出来的,所以我们也可以尝试使用其它的SPI接口,比如SPI2,又或者,我们可以尝试使用其它IO作为片选引脚,比如使用GPIO1_IO01,此时就只需要把IO模块中的12号引脚也即UART2_TXD接到IO模块的17号引脚GPIO1_IO01即可,可以试一试。

3.2.1 设备树
在cortex架构的芯片中,对于外设通常会提供控制器,这样能够让外设和CPU异步执行,只需要CPU发指令给芯片的外设控制器,控制器就会自动工作而无需CPU的干预。对于ixm6ull,它是属于cortex-A架构的芯片,也提供了SPI控制器,已经在本文的前面部分介绍过。因此,对于SPI驱动,设备树通常包括两部分,一部分是主控芯片的spi控制器的设备树代码,另一部分是spi外设的设备树代码,所以相应的,驱动程序也分为两部分,一部分是主控芯片的spi控制器驱动,主要提供的就是主控芯片的SPI控制器的收发功能,其实就是把前面介绍的SPI控制器裸机驱动进行更加全面的升级改造,并且写成驱动程序形式提供给外设的SPI驱动程序使用,另一部分就是SPI外设的驱动编写,主要就是利用SPI控制器驱动提供的接口来向外设寄存器写入数据或者读取外设寄存器的数据来实现对SPI外设的控制,这部分就是需要结合具体外设的参考手册和数据手册来进行了,比如本文中的ICM20608传感器就是一个很复杂的SPI外设,其内部的寄存器有一百多个,要写好它的驱动程序并不容易,所以本文中的驱动程序也只是能够简单的用起来,重点在于通过这个外设学习嵌入式Linux的SPI驱动开发流程。对于主控芯片的SPI控制器驱动,通常由芯片厂家提供,毕竟厂家的驱动工程师最了解他们的芯片。但是无论芯片怎么变化,其核心原理都是不会改变的,不同的芯片其实都是慢慢迭代更新的,不可能每一代都有巨大的差异。
imx6ull芯片厂商的出厂evk开发板中禁止了ECSPI3控制器节点的驱动,需要重新使能,至于如何写SPI控制器的设备树代码以及SPI外设的设备树代码,可以参考原厂设备树中的已有内容,在 imx6qdl-sabresd.dtsi这个头文件中,有这么一段代码:

这是imx6ull中ecspi1控制器的设备树代码,对于其它设备树代码,其驱动程序和设备树代码除了所用引脚不同,肯定也是一样的,所以可以仿照此段代码写ecspi3控制器的设备树代码。分析上面的代码,可以发现它用到了pinctrl子系统和gpio子系统,查看pinctrl_ecspi1的内容,会发现就是类似的引脚复用的设备树代码。
在imx6ull的出厂evk开发板的Linux系统中,其实都写好了spi控制器的驱动,只不过还没有使能,在imx6ull.dtsi文件中查询ecspi3,可以看到如下代码:

上述代码就是ecspi3控制器的驱动程序对应的设备树代码,可以看到里面定义reg,clocks等属性,至于这些属性的及其对应的作用,那就由编写这个驱动的工程师决定了,可以看到,其中的status属性是disabled,因此,在我们自己的设备树中,要设置status = "okay"
实际上,在原厂开发板的Linux系统提供的ecspi控制器的驱动程序对应的设备树是不完整的,还没有对控制器的引脚进行复用,因此首先需要使用pinctrl子系统和gpio子系统在设备树中对要复用为spi的引脚进行指定。
利用ixm6ull芯片厂商提供的imx pins tool工具可以方便的得到引脚复用为对应功能时的pinctrl节点的设备树代码。ECSPI3控制器使用UART2_TXD、UART2_RXD、UART2_RTS、UART2_CTS这四个引脚,需要配置为ECSPI3功能,所以在imx pins tool工具中找到ecspi3,并且选择对应的几个引脚,如图所示:

在iomuxc节点下就生成了OARD_InitPins: BOARD_InitPinsGrp 节点,我们只需要把这个节点的内容复制到我们的设备树文件中的iomux节点下作为一个子节点并修改名字即可。实际上,我们会把MX6UL_PAD_UART2_TX_DATA复用为GPIO功能,但是在上图中因为是在ECSPI3中选中该引脚,因此是被复用为ECSPI3_SS0功能,所以我们需要把它在ECSPI3中取消,然后去对应的GPIO组中找到这个引脚并选中。查看UART2_TX_DATA引脚的复用寄存器,可以看到它可以被复用为GPIO1_IO20,所以去GPIO1组中选中IO20

再结合前面在ECSPI3中选中的3个引脚,可以得到最终的pinctrl子系统的子节点设备树代码如下:

点击查看代码
pinctrl_ecspi3: ecspi3Grp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI         0x000010B0
                MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO         0x000010B0
                MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK       0x000010B0
                MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20        0x000010B0
            >;
        };

完成了pinctrl子节点的设备树代码后,最终得到的ECSPI3控制器和ICM20608从设备的设备树代码如下:

点击查看代码
&ecspi3 {
	fsl,spi-num-chipselects = <4>;
	cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>, <&gpio1 1 GPIO_ACTIVE_LOW>, <&gpio1 10 GPIO_ACTIVE_LOW>, <&gpio1 2 GPIO_ACTIVE_LOW>;	/*注意,低电平有效,意味着逻辑值和物理值相反*/
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;	/*使用pinctrl子系统复用引脚*/
	status = "okay";	/*因为再imx6ulldtsi中ecspi3这个节点被disabled了,这里就要重新使能*/

	icm20608: icm20608@0 {
		compatible = "icm20608_driver";
		spi-max-frequency = <2000000>;	/*设置SPI时钟频率上限,icm20608最高支持8M,因此这里可以设置2M*/
		reg = <3>;	/*使用控制器的通道0,猜测,跟片选引脚在cs-gpios中的位置序号有关*/
	};
};

其中fsl,spi-num-chipselects表示有多少个片选引脚,每个片选引脚对应一个通道,icm20608节点中的reg就表示使用哪个片选引脚也即哪个通道。这里有4个引脚,是我为了验证可以自由选择片选引脚而加上去做实验用的,使用其它片选引脚时,需要使用杜邦线把对应的IO引脚和ICM20608的片选引脚连接起来,否则无法进行SPI通信。
注意:编写完设备树代码后,全局搜索以下用到的节点,确定是否有引脚冲突,如果有冲突,把对应的冲突引脚信息注释掉。

3.2.2 驱动程序
在内核中尽量不要使用浮点数,如果非要使用,那需要开启浮点功能,但是我还不懂怎么开。
驱动程序的开发原则就是,驱动程序提供功能,至于用哪些功能,由上层应用程序自由选择。
仍然遵循的是总线设备驱动模型,核心仍然是file_operation结构体,只不过在设备树中spi从设备节点是转换为spi_client,并且使用spi_driver结构体而不再是platform_driver结构体,但是这些结构体的内容和用法其实都是类似的。
在实现各个函数时,要体现面向对象的编程思想,先列出函数框架,确定有哪些功能模块,再去逐一实现,对于通用的代码,要封装成为函数,以避免代码冗余。
驱动程序代码如下:

点击查看代码
#include "asm-generic/current.h"
#include "asm-generic/errno-base.h"
#include "asm-generic/poll.h"
#include "asm-generic/siginfo.h"
#include "asm/signal.h"
#include "asm/string.h"
#include "asm/uaccess.h"
#include "linux/err.h"
#include "linux/export.h"
#include "linux/gpio/driver.h"
#include "linux/i2c.h"
#include "linux/irqreturn.h"
#include "linux/kdev_t.h"
#include "linux/mod_devicetable.h"
#include "linux/nfs_fs.h"
#include "linux/of.h"
#include "linux/printk.h"
#include "linux/socket.h"
#include "linux/wait.h"
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/ktime.h>
#include <linux/delay.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
#include <linux/workqueue.h>
#include <asm/current.h>
#include <linux/spi/spi.h>
#include "icm20608.h"

/*
ICM20608传感器的SPI驱动程序,基于设备树的总线设备驱动模型,设备节点在设备树中SPI控制器下定义
本文件实现:
1 file_operations结构体
    1.1 read 函数 读取传感器数据
    1.2 open 函数 初始化ICM20608,初始化中只配置了基础功能
    1.3 write 函数 配置ICM20608其他功能
    1.4 release 函数 关闭传感器,减少功耗

2 spi_driver结构体
    2.1 probe函数 驱动和设备匹配时执行,可以从这个函数获取设备节点信号,在函数中进行字符设备驱动注册的流程
    2.2 remove函数 执行一些销毁操作
    2.3 driver字段
    2.4 id_table字段

3 入口函数
    注册spi_driver

4 出口函数
    撤销spi_driver
*/

struct icm20608_dev_struct{

    struct spi_device *icm20608_spi_dev; //记录设备树转换的icm20608设备节点
    /*加速度adc值*/
    short accel_x_adc;
    short accel_y_adc;
    short accel_z_adc;
    short accel_scale; //加速度计量程
    /*温度adc值*/
    short temperature_adc;

    /*陀螺仪adc值*/
    short gyro_x_adc;
    short gyro_y_adc;
    short gyro_z_adc;
    short gyro_scale;  //陀螺仪量程
};

static int major = 0;;  //主设备号
static struct class *icm20608_class;    //设备类
static struct icm20608_dev_struct icm20608_dev;    //icm20608设备


static int icm20608_read_regs(struct spi_device *spi, unsigned char addr, unsigned int len, unsigned char *buf)
{
    /*读取addr开始的多个寄存器的多个字
    addr: 寄存器首地址
    len: 读取的字节长度
    buf: 读取的内容保存在buf中
    */
    int err;
    struct spi_transfer transfers[1];
    unsigned char rx_data[len+1];   //读取的结果保存在这个缓存中
    unsigned char tx_data[len+1];   //tx_data[0]是寄存器首地址
    
    tx_data[0] = addr | (0x80); //最高位写1,读

    memset(rx_data, 0, sizeof(rx_data));
    /*初始化transfers*/
    memset(transfers, 0, sizeof(transfers));

    transfers[0].tx_buf = tx_data;
    transfers[0].rx_buf = rx_data;
    transfers[0].len = len + 1;

    err = spi_sync_transfer(spi, transfers, 1);   //同步spi传输,发送len+1字节同时在rx_data受到len+1个字节
    if(err < 0)
    {
        printk("%s line %d, transfer err! err: %d\n", __FUNCTION__, __LINE__, err);
        return err;
    }
    memcpy(buf, rx_data + 1, len);  //rx_data从1开始的字节才是接收到的数据

    return 0;
}

static int icm20608_write_regs(struct spi_device *spi, unsigned char addr, unsigned int len, char *buf)
{
    /*写多个寄存器,icm20608支持连续写多个寄存器,每次写后寄存器地址自动加1
    addr 8位寄存器首地址,最高位是寄存器的读写控制位
    len 要写入的字节数量
    buf 要写入的数据
    */
    int err;
    struct spi_transfer transfers[1];
    unsigned char tx_data[len + 1]; //发送字节内容,tx_data[0]存放寄存器地址,最高位是读写控制位
    tx_data[0] = addr & (~0x80); //最高位0,写
    memcpy(tx_data+1, buf, len);    //把要写的内容放到发送缓存区

    /*初始化transfer结构体*/
    memset(transfers, 0, sizeof(transfers));    //清零,这部分是防止内存违法操作
    
    transfers[0].tx_buf = tx_data;
    transfers[0].len = len + 1; //寄存器地址 + 要写入的len字节,因此是len+1

    err = spi_sync_transfer(spi, transfers, 1);
    if(err < 0)
    {
        printk("%s line %d, transfer err! err: %d\n", __FUNCTION__, __LINE__, err);
        return err;
    }
    return 0;
}

static int icm20608_write_one_reg(struct spi_device *spi, unsigned char reg_addr, unsigned char reg_data)
{
    /*向寄存器写一个字节
    reg_addr: 8位寄存器地址
    reg_data: 要写入的8位数据
    */
    int err;
    err = icm20608_write_regs(spi, reg_addr, 1, &reg_data);
    if(err)
    {
        printk("%s line %d, write err! err: %d\n", __FUNCTION__, __LINE__, err);
    }
    return 0;
}

static unsigned char icm20608_read_one_reg(struct spi_device *spi, unsigned char reg_addr)
{
    /*读一个寄存器
    reg_addr: 要读的寄存器地址
    返回读取到的字节
    */
    unsigned char ret;
    int err;
    err = icm20608_read_regs(spi, reg_addr, 1, &ret);
    if(err)
    {
        printk("%s line %d, read err! err: %d\n", __FUNCTION__, __LINE__, err);
    }
    return ret;
}


static void icm20608_init(struct icm20608_dev_struct *dev)
{
    /*icm20608初始化函数,通过向寄存器写入内容实现基本功能的初始化*/
    /*复位*/
    int icm20608_id;
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_PWR_MGMT_1, 0x80);
    mdelay(50); //一般复位后都需要等待一下
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_PWR_MGMT_1, 0x01);
    mdelay(50);

    /*读IIC设备号,虽然SPI中用不到,但是也可以看一下*/
    icm20608_id = icm20608_read_one_reg(dev->icm20608_spi_dev, ICM20_WHO_AM_I);
    printk("icm20608_id = %d\n", icm20608_id);

    /*设置输出速率,也即内部采样速率*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_SMPLRT_DIV, 0x00);
    /*设置陀螺仪量程*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_GYRO_CONFIG, 0x18); //+-2000dps量程
    dev->gyro_scale = 2000;

    /*设置加速度计量程*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_ACCEL_CONFIG, 0x18);    //+-16G量程
    dev->accel_scale = 16;

    /*陀螺仪低通20Hz滤波*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_CONFIG, 0x04);
    /*设置加速度低通滤波21.2Hz*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_ACCEL_CONFIG2, 0x04);

    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_PWR_MGMT_2, 0x00);  //打开所有加速度和陀螺仪    
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_LP_MODE_CFG, 0x00); //关闭低功耗
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_FIFO_EN, 0x00);  /*关闭FIFO*/

}

static int icm20608_close(struct icm20608_dev_struct *dev)
{
    /*复位*/
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_PWR_MGMT_1, 0x80);
    mdelay(50);
    icm20608_write_one_reg(dev->icm20608_spi_dev, ICM20_PWR_MGMT_1, 0x40);
    mdelay(50);
    return 0;
}

static void icm20608_read_data(struct icm20608_dev_struct *dev, unsigned char addr, unsigned int len)
{
    /*读取全部传感数据
    spi: 从设备
    addr: 寄存器首地址,在icm20608中,数据寄存器的地址连在一起,因此可以连续读
    len: 读的长度
    */
    unsigned char data[14];
    icm20608_read_regs(dev->icm20608_spi_dev, ICM20_ACCEL_XOUT_H, 14, data);
    /*将数据保存到dev中*/
    dev->accel_x_adc = (short) ((data[0] << 8) | data[1]);
    dev->accel_y_adc = (short) ((data[2] << 8) | data[3]);
    dev->accel_z_adc = (short) ((data[4] << 8) | data[5]);
    dev->temperature_adc = (short) ((data[6] << 8) | data[7]);
    dev->gyro_x_adc = (short) ((data[8] << 8) | data[9]);
    dev->gyro_y_adc = (short) ((data[10] << 8) | data[11]);
    dev->gyro_z_adc = (short) ((data[12] << 8) | data[13]);
    printk("accel_x_adc: %d, %d, %d\n", dev->accel_x_adc, dev->accel_y_adc, dev->accel_z_adc);
    printk("temp_adc: %d\n", dev->temperature_adc);
    printk("gyro_scale = %d\n", dev->gyro_scale);

}

int icm20608_open(struct inode *node, struct file *file)
{
    /*初始化icm20608*/
    // int err;
    icm20608_init(&icm20608_dev);  //初始化

    return 0;
}



ssize_t icm20608_read(struct file *file, char __user *user_buf, size_t size, loff_t *offset)
{
    /*读传感器数据*/
    int err;
    short data[9];
    icm20608_read_data(&icm20608_dev, 0x3B, 14);
    data[0] = icm20608_dev.accel_x_adc;
    data[1] = icm20608_dev.accel_y_adc;
    data[2] = icm20608_dev.accel_z_adc;
    data[3] = icm20608_dev.accel_scale;

    data[4] = icm20608_dev.gyro_x_adc;
    data[5] = icm20608_dev.gyro_y_adc;
    data[6] = icm20608_dev.gyro_z_adc;
    data[7] = icm20608_dev.gyro_scale;

    data[8] = icm20608_dev.temperature_adc;

    err = copy_to_user(user_buf, data, sizeof(data));
    if(err)
    {
        printk("copy to user erro. err = %d\n", err);
        return -err;
    }
    return size;

}

ssize_t icm20608_write(struct file *file, const char __user *user_buf, size_t size, loff_t *offset)
{
    /*向icm20608写,暂时不实现*/
    return 0;
    
}
int icm20608_release(struct inode *node, struct file *file)
{
    /*关闭icm20608*/
    icm20608_close(&icm20608_dev);
    return 0;
}

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

static int icm20608_probe(struct spi_device *spi)
{
    /*当驱动和设备匹配时执行*/
    
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    icm20608_dev.icm20608_spi_dev = spi;    //记录设备节点

    /*注册驱动*/
    major = register_chrdev(major, "icm20608_spi_driver", &icm20608_oprs);

    /*创建设备类*/
    icm20608_class = class_create(THIS_MODULE, "icm20608_class");
    if(IS_ERR(icm20608_class))
    {
        printk("%s %s line %d, icm20608_class create failed.\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev(major, "icm20608_spi_driver");
        return PTR_ERR(icm20608_class);
    }
    /*创建设备节点*/
    device_create(icm20608_class, NULL, MKDEV(major, 0), NULL, "icm20608");

    return 0;
}

static int icm20608_remove(struct spi_device *spi)
{
    /*卸载驱动时执行*/
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

    device_destroy(icm20608_class, MKDEV(major, 0));

    class_destroy(icm20608_class);

    unregister_chrdev(major, "icm20608_spi_driver");

    return 0;
}

static struct spi_device_id icm20608_spi_ids[] = {
    {.name = "icm20608,spi"},
};

static struct of_device_id	icm20608_match[] = {
    {.compatible = "icm20608_driver"},
};


static struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608,spi",
        .of_match_table = icm20608_match
    },
    .id_table = icm20608_spi_ids,
};

static int __init icm20608_driver_init(void)
{
    int err;
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    err = spi_register_driver(&icm20608_driver);
    if(err)
    {
        printk("%s line %d, spi register failed!\n", __FUNCTION__, __LINE__);
        return -err;
    }
    return 0;
}

static void __exit icm20608_driver_exit(void)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_driver_init);
module_exit(icm20608_driver_exit);
MODULE_LICENSE("GPL");

编写好设备树和驱动程序后,开启开发板,如果设备树正确,那么应该可以在开发板的Linux操作系统中看到spi控制节点和对应的从设备


如上图所示,进入/sys/bus/spi/devices目录ls,可以看到spi2.3,在设备树中序号从1开始,而在内核中序号从0开始,因此ecspi3对应内核中spi2,看前面的设备树代码,icm20608节点中reg是3,因此是spi2.3,说明内核成功解析了该spi从设备,可以进入spi2.3目录查看目录下面各个属性,可以验证和设备树中的是否一致。

3.2.3 应用程序
应用程序中可以使用浮点数,操作系统可以通过整数来模拟支持
应用程序代码如下:

点击查看代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>

/*使用方法:
./icm20608_test <dev>
*/

int main(int argc, char *argv[])
{
	int fd;
	short data[9];
	short scale_accel, scale_gyro;
	float gyro_x_act, gyro_y_act, gyro_z_act;
	float accel_x_act, accel_y_act, accel_z_act;
	float temp_act;
	float sensitivity_accel, sensitivity_gyro;
	int ret;
	if(argc != 2)
	{
		printf("Usage: %s <dev>\n", argv[0]);
		return -1;
	}
	fd = open(argv[1], O_RDWR);
	if(fd < 0)
	{
		printf("open %s erro.\n", argv[1]);
		return -1;
	}

	while(1)
	{
		ret = read(fd, data, sizeof(data));
		if(ret == -1)
		{
			printf("read err., ret = %d\n", ret);
			return -1;
		}
		scale_accel = data[3];
		scale_gyro = data[7];
		sensitivity_accel = (float)(65536.0 / (float)(2 * scale_accel));
		sensitivity_gyro = (float)(65536.0 / (float)(2 * scale_gyro));
		// printf("scale_accel = %d, data[7] = %d, scale_gyro = %d, sensitivity_accel = %f, sensitivity_gyro = %f\n", scale_accel, data[7], scale_gyro, sensitivity_accel, sensitivity_gyro);

		/*计算真实值*/
		accel_x_act = (float)(data[0]) / sensitivity_accel;
		accel_y_act = (float)(data[1]) / sensitivity_accel;
		accel_z_act = (float)(data[2]) / sensitivity_accel;

		gyro_x_act = (float)(data[4]) / sensitivity_gyro;
		gyro_y_act = (float)(data[5]) / sensitivity_gyro;
		gyro_z_act = (float)(data[6]) / sensitivity_gyro;

		temp_act = ((float)(data[8]) - 25) / 326.8 + 25;

		printf("accel_x_act = %f, accel_y_act = %f, accel_z_act = %f\n", accel_x_act, accel_y_act, accel_z_act);
		printf("gyro_x_act = %f, gyro_y_act = %f, gyro_z_act = %f\n", gyro_x_act, gyro_y_act, gyro_z_act);
		printf("temperature: %f\n", temp_act);

		usleep(200000);	//休眠100ms
	}

	close(fd);

	return 0;
}

应用程序通过系统调用read最终调用编写的ICM20608的驱动程序中的read函数,读取传感器的加速度、陀螺仪和温度的ADC采样值,并转换为真实值,ADC采样值和真实值的转换关系可以参考ICM20608的芯片参考手册。


如图加速计的ADC值和真实值之间的转换关系,在量程为+-16g时就是2048/g

如图为陀螺仪的ADC值与真实值之间的转换关系,在量程为+-2000时就是16.4Lsb/(。/s)

注意,ADC采样值是用无符号数表示还是有符号数表示,其实无关紧要,只要你理解其中的转换规则就行。通常来说,对于对称的正负量程,用有符号数来表示方便一些,否则还需要进行转换,以加速度计为例,16位ADC采样值用有符号整数类型short表示,那么0 到 32768表示的就是0到16g,32769 到 65536 其实对应有符号数的-32768 到 -1,负数用补码来表示。

最终的实验效果如下所示:

补充思考:
在所用的imx6ull开发板中,使用的Linux操作系统是基于官方的EVK开发板进行适配的,但是这个开发板并没有SPI设备,因此SPI节点是默认不开启的,但是也还是提供了SPI子系统。问题:如果官方某一款芯片的适配的Linux系统不提供SPI子系统,那么该怎么办?解决办法有两个:(1)把裸机代码按照驱动框架移植成Linux驱动代码,在操作系统中对相关寄存器进行操作;(2)仿照SPI子系统写一套SPI子系统,其实也涉及了对相关寄存器的操作,可以参考上下分层,左右分离的驱动开发流程,关于spi控制器的驱动程序提供SPI的操作方法,然后具体的SPI设备在其驱动程序中调用SPI控制器的驱动程序提供的函数接口。实际上的SPI子系统也是这么开发的。所用Linux驱动其实就等于驱动框架 + 裸机操作。

标签:include,SPI,六轴,dev,spi,MEMS,icm20608,data
From: https://www.cnblogs.com/starstxg/p/18261894

相关文章

  • 基于dspic33ck64mp105的电机控制器开发①
    原理图是基于microchip官方的MCP1722_Power_Tools参考设计而来,修改部分如下:https://www.microchip.com/en-us/tools-resources/reference-designs/portable-power-tool-reference-design1,修改了电源模块2,修改了栅极驱动3,增加了蓝牙通信模块4,修改了原版AUX的端口5,增加了一个LED......
  • [Aspire] Run session could not be started
    ErrordetailsRunsessioncouldnotbestarted:failedtoconnectedtoIDErunsessionnotificationendpoint:tls:failedtoverifycertificate:x509:certificatehasexpiredorisnotyetvalidSolutionRunningthedotnetdev-certscommandsbelowtore-g......
  • SpingBoot原理
    配置优先级SpringBoot配置的优先级从高到低依次为命令行参数、JNDI属性、Java系统属性、操作系统环境变量、外部配置文件、内部配置文件、注解指定的配置文件和编码中直接指定的默认属性。具体如下:命令行参数:启动应用时,通过命令行指定的参数拥有最高优先级。例如,使用--server......
  • 设计NOR Flash(SPI接口)的Flashloader(MCU: stm32f4)
    目录概述1软硬件1.1软硬件信息表1.2NORFlash芯片(W25Q64BVSSI)1.2.1W25Q64BVSSI芯片介绍1.2.2NORFlash接口1.3MCU与NORFlash接口2SPIFlash功能实现2.1软件框架结构2.2代码实现2.2.1 Dev_Inf文件2.2.2W25QXX驱动程序2.3Flashloader驱动接口程序3K......
  • IMX6ULL开发板spi OLED驱动
    本文是IMX6ULL开发板spiOLED驱动学习笔记,方便后面查看时快速的回顾,而不需要一点点的看视频视频地址:https://www.bilibili.com/video/BV1Yb4y1t7Uj?p=144&spm_id_from=pageDriver&vd_source=1d93d6a5e22d4b223c6c3ac4f5727eb8视频选集:P141-P1501、将文件上传到虚拟机共享目......
  • STM32通过SPI硬件读写W25Q64
    文章目录1. W25Q642.硬件电路3. 软件/硬件波形对比4.STM32中的SPI外设5.代码实现5.1MyI2C.c5.2 MyI2C.h5.3W25Q64.c5.4 W25Q64.h5.5 W25Q64_Ins.h5.6main.c1. W25Q64对于SPI通信和W25Q64的详细解析可以看下面这篇文章STM32单片机SPI通信详解-CSDN......
  • STM32通过SPI软件读写W25Q64
    文章目录1.W25Q642.硬件电路3. W25Q64框架图4. 软件/硬件波形对比5.代码实现5.1MyI2C.c5.2 MyI2C.h5.3W25Q64.c5.4 W25Q64.h5.5 W25Q64_Ins.h5.6main.c1.W25Q64对于SPI通信和W25Q64的详细解析可以看下面这篇文章STM32单片机SPI通信详解-CSDN博客......
  • 通讯协议大全(UART,RS485,SPI,IIC)
    参考自: 常见的通讯协议总结(USART、IIC、SPI、485、CAN)-CSDN博客UART那么好用,为什么单片机还需要I2C和SPI?_哔哩哔哩_bilibili5分钟看懂!串口RS232RS485最本质的区别!_哔哩哔哩_bilibili喜欢几位博主老师老师的还请看原贴/原视频数据通信 数据通信是指通过某种传......
  • [题解]AT_abc236_f [ABC236F] Spices
    思路首先对所有的\(c\)从小到大排序,然后对于每一个值如果之前能凑出就不选,否则就选。这样做显然是对的。令\(p_1,p_2,\dots,p_{2^n-1}\)表示将\(c\)排序之后,对应原来的下标;\(S\)表示选出数的集合;\(S'\)表示最终选出数的集合。可以证明两个问题:如果\(p_i\)可以被已选......
  • STM32单片机SPI通信详解
    文章目录1.SPI通信概述2.硬件电路3.移位示意图4.SPI时序基本单元5.SPI时序6.Flash操作注意事项7.SPI外设简介8.SPI框图9.SPI基本结构10. 主模式全双工连续传输11. 非连续传输12. 软件/硬件波形对比13.代码示例1.SPI通信概述SPI(SerialPeriphera......