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

news/2024/7/4 12:02:32/文章来源:https://www.cnblogs.com/starstxg/p/18261894

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	0x20e00A0static 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:使能SPIbit3:写入TXDATA之后,立即发送bit4:设置通道0为master modebit31: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中,我们得知时钟来源为PLL31、pll3_sw_clk_sel为0,则选择pll3;为1则选择ccm_pll3_bys,时钟   默认选择pll3 。输出pll3_sw_clk给spi进行使用  输出给spi的时钟为480M/8=60Mhz2、我们需要使能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) = 4Mprintf("spi 初始化结束\n\r"); //引脚初始化iomuxc_sw_set(UART2_TX_BASE,5);//设置为GPIO作为片选来进行使用。GPIO1_IO20GPIO1_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 modespi_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);//采样率默认1Kicm20608g_write_addr(ICM20608G_CONFIG, 0x00);//禁止FIFOicm20608g_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);//禁止传感器FIFOicm20608g_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         0x000010B0MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO         0x000010B0MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK       0x000010B0MX6UL_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_driver4 出口函数撤销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支持连续写多个寄存器,每次写后寄存器地址自动加1addr 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+1err = 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驱动其实就等于驱动框架 + 裸机操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/736043.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于dspic33ck64mp105的电机控制器开发①

原理图是基于 microchip官方的 MCP1722_Power_Tools 参考设计而来,修改部分如下:https://www.microchip.com/en-us/tools-resources/reference-designs/portable-power-tool-reference-design1,修改了电源模块2,修改了栅极驱动3,增加了蓝牙通信模块4,修改了原版AUX的端口…

7、 Django-路由-router-页面跳转

概念: 在实际开发过程中、一个Django项目会包含很多的app、这时候如果我们只在主路由里进行配置就会显得杂乱无章、 所以通常在每个app中创建各自的urls.py路由模块、然后从根路由出发、将app所属的url请求、全部转发到相应的urls.py 模块 而这个从主路由转发到各个应用的路由…

10、 Django-模板-templates

模板语法 #模板中的变量语法:{{ var }}如果变量不存在、则插入空字符串#方法不能有参数{{ int }}{{ str }}{{ list }}{{ list.0 }}{{ dict }}{{ dict.a }} #dict[a]{{ func }} #传递函数{{ class_obj.func }} #传递类.方法#列表、使用索引、不允许负索引it…

Simple WPF: WPF 透明窗体和鼠标事件穿透

一个自定义WPF窗体的解决方案,借鉴了吕毅老师的WPF制作高性能的透明背景的异形窗口一文,并在此基础上增加了鼠标穿透的功能。可以使得透明窗体的鼠标事件穿透到下层,在下层窗体中响应。一个自定义WPF窗体的解决方案,借鉴了吕毅老师的WPF制作高性能的透明背景的异形窗口一文…

VMware安装Win11环境

准备 Win11的iso镜像 下载链接:https://www.microsoft.com/zh-cn/software-download/windows11/?open_in_browser=true 配置步骤 步骤一——创建虚拟机 1、点击创建新虚拟机2、使用典型模式3、选择镜像位置4、选择虚拟机存放位置5、输入密码,此密码可以随便写6、选择单个文件…

2、爬虫-安装anaconda工具

1、官网:https://www.anaconda.com/download-success2、一直下一步安装即可 3、打开4、输入:jupyter notebook 打开有一个浏览器的界面 5、右上角点击new新建python3(ipykernel)本文来自博客园,作者:little小新,转载请注明原文链接:https://www.cnblogs.com/littlecc/…

50、k8s-DashBoard(k8s的web)-部署

1、下载yaml文件:https://github.com/kubernetes/dashboard/blob/v2.0.0/aio/deploy/recommended.yaml 2、修改yaml文件的service 配置: --------------------------------------------- ---kind: Service apiVersion: v1 metadata:labels:k8s-app: kubernetes-dashboardname…

[JLU] 数据结构与算法上机题解思路分享-第二次上机

这是吉林等通知大学数据结构与算法上机题的题解思路,没有精妙的解法,只是一个记录罢前言 首先,请务必自己尽全力尝试实现题目,直接看成品代码,思维就被拘束了,也很容易被查重。 这里只是思路解析的博客,代码仓库在 JLU_Data_Structures_Record 希望你能在这里找到你想要…

docker 基础学习--尚硅谷教程

1、新建、启动容器docker run [OPTIONS] IMAGE [COMMAND] [ARG...] 2、列出当前所有正在运行的容器docker ps [OPTIONS] 3、退出容器4、重启、退出、删除容器 5、启动守护式容器 docker 常见命令

Bruno安装使用

下载地址直接解压,右键Bruno.exe创建快捷方式创建集合右键集合,New Request指定名称和url指定参数保存并测试

《与光重聚》 —— 属于他的夏日花火

题目是歌曲名称,from 《夏日花火》 (国产galgame ; 视觉小说「夏日花火」原声音乐集 - dizzylab PV视频 因为被平衡树制裁了,所以心血来潮写这篇文章,姑且算是练笔吧(毕竟好久不写鲜花了; 游戏本体发行时间:2022年10月28日,目前无DLC(永远的遗憾; 游戏背景设计是D…

玄机-第一章 应急响应-Linux日志分析

玄机-第一章 应急响应-Linux日志分析 账号root密码linuxrz ssh root@IP 1.有多少IP在爆破主机ssh的root帐号,如果有多个使用","分割 2.ssh爆破成功登陆的IP是多少,如果有多个使用","分割 3.爆破用户名字典是什么?如果有多个使用","分割 4.登陆…

使用 .NET 构建 UI 界面的各种方式

微软搞出了很多构建 UI 程序的框架,如 WinForms WPF WinUI MAUI,他们之间的简单对比可以看如下这篇官方文档 Overview of framework options - Windows apps | Microsoft Learn 本文主要是记录一下在搜索相关问题时,了解到的内容,不一定准确,如果发现错误,请留言补充。 1…

Docker详细安装教程

安装Docker: # 1,, 卸载旧的版本# 2,需要的安装 yum install -y yum-utils# 3, 设置镜像的仓库 https://blog.csdn.net/qq_43168442/article/details/116770163 (访问这个博客网站进行配置)# 更新yum软件包索引 yum makecache fast# 4,安装docker docker-ce 社区 ee企业…

详细讲解 Keil Pack Installer,以及通过 Keil 官网获取 Pack

前言 大家好,我是梁国庆。 收到粉丝留言,说 Keil 安装 Pack 不太明白,可不可以详细演示一下?当然可以有,直接视频+文章全部安排,我就是宠粉。 PS:第一次录视频有些紧张,见谅哈。微信视频号:https://weixin.qq.com/sph/AXbpYwEaw b站:https://www.bilibili.com/video…

webdav协议及我的笔记方案(私有部署)

背景 用markdown用于文章写作,有几年时间了,不是很喜欢折腾,主要就是在电脑上写,用的笔记软件就是typora。由于里面有很多工作相关的,以及个人资料相关的(包含了各种账号、密码啥的),所以不敢往各种云服务上放,还是想着数据由自己来管着。 自己管数据的话,就是数据存…

模拟集成电路设计系列博客——8.3.2 PLL中的抖动与相位噪声

8.3.2 PLL中的抖动与相位噪声 在PLL中有若干种抖动源,具体来说包括:输入参考的抖动\(\phi_{in}\) VCO中的抖动 环路滤波器产生的噪声 分频器产生的噪声由于任何实际PLL中的抖动都相对较小,因此分析其在环路中和环路内的传播可以使用线性小信号模型。上面列出的噪声源出现在环…

【git】github如何上传超过100MB大小的单个文件

在使用 GitHub 进行版本控制时,默认情况下,单个文件的大小限制为 100MB。 如果你需要上传超过这个大小的文件,可以使用 Git LFS(Large File Storage)。 Git LFS 是一种 Git 扩展,专门用于处理大文件,它将大文件替换为轻量级的指针,并将实际的文件内容存储在远程服务器上…

ros2 - microros - 雷达 -可视化点云

上一节完成了指定角度距离的测量这一节我们将其合成ROS的laserscan消息,并将其通过microros发布到上位机,最终实现rviz2的可视化。 一、雷达消息介绍使用指令ros2 interface show sensor_msgs/msg/LaserScan,可以看到ROS2对雷达数据接口的定义。# Single scan from a planar…

玄机流量特征分析-蚁剑流量分析

玄机流量特征分析-蚁剑流量分析 1.木马的连接密码是多少 2.黑客执行的第一个命令是什么 3.黑客读取了哪个文件的内容,提交文件绝对路径 4.黑客上传了什么文件到服务器,提交文件名 5.黑客上传的文件内容是什么 6.黑客下载了哪个文件,提交文件绝对路径1.过滤http,发现连接密码…