ARM --- SPI通信协议简介,IMX6ULL使用SPI与ADXL345传感器进行数据通信 一. SPI 简介串行 同步 全双工1.1 简介SPI全称是Serial Perripheral Interface也就是串行外围设备接口。SPI 是Motorola公司推出的一种同步串行接口技术是一种高速、全双工的同步通信总线SPI 时钟频率相比 I2C 要高很多最高可以工作在几MHz几十MHz甚至上百 MHz。SPI 以主从方式工作通常是有一个主设备和一个或多个从设备一般 SPI 需要 4 根线但是也可以使用三根线(单向传输)1.2 SPI 四根线①、CS/SS(Slave Select/Chip Select)这个是片选信号线用于选择需要进行通信的从设 备。I2C 主机是通过发送从机设备地址来选择需要进行通信的从机设备的SPI 主机不需要发送从机设备直接将相应的从机设备片选信号拉低即可②、SCLK(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 线连接多个从设备的结构如图所示1.3 SPI 的四种工作模式SPI 有四种工作模式通过串行时钟极性(CPOL)和串行时钟相位(CPHA)的搭配来得到四种工作模式串行时钟极性CPOL(Clock Polarity)CPOL 0代表串行时钟线空闲状态为低电平CPOL 1代表串行时钟线空闲状态为高电平串行时钟相位CPHA(Clock Phase)CPHA 0代表在串行时钟线的第一个跳变沿进行数据采集CPHA 1代表在串行时钟线的第二个跳变沿进行数据采集可知通过组合SPI 总共有四种工作模式CPOL CPHA1 0SCLK空闲为高电平SPI下降沿采样1 1SCLK空闲为高电平SPI上升沿采样0 0SCLK空闲为低电平SPI上升沿采样0 1SCLK空闲为低电平SPI下降沿采样四种工作模式如下图所示CPOL 0, CPHA 1,这个工作模式的时序图如下图所示从图可以看出SPI 的时序图很简单不像 I2C 那样还要分为读时序和写时序因为 SPI 是全双工的所以读写时序可以一起完成。图中CS 片选信号先拉低选中要通信的从设备然后通过MOSI和MISO这两根数据线进行收发数据MOSI 数据线发出了 0XD2 这个数据给从设备同时从设备也通过 MISO 线给主设备返回了 0X66 这个数据注意1、SPI协议与I2C不同的是I2C协议规定了数据传输方式是高位先行(MSB)而SPI则是根据所选的从机不同有不同的规定但是大多情况下(90%)都是高位先行(MSB)2、数据的收发必须建立在片选线为低电平的前提下1.4 SPI 与 I2C 的区别1、I2C 固定两根线SCL 与 SDASPI 三或者四根线SCLKCSMOSIMISO2、I2C通过七位从机地址对从机进行寻址SPI通过将对应的片选电平拉低拉选择与哪个从机进行通信3、I2C 标准速率 100K高速为400KSPI 速率可达M几十或者几百MHz4、I2C:半双工 同步 串行SPI:全双工 同步 串行二. IMX6ULL 的 SPI 模块2.1 IMX6ULL 的 ECSPI 简介IMX6ULL 自带的 SPI 外设叫做 ECSPI全称是 Enhanced Configurable Serial Peripheral Inte rface。ECSPI 有64*32 个接收 FIFO(RXFIFO)和64*32 个发送 FIFO(TXFIFO)IMX6ULL 有 4 个 ECSPI每个 ECSPI 支持四个片选信号ECSPI 特性如下①、全双工同步串行接口。②、可配置的主/从模式。③、四个片选信号支持多从机。④、发送和接收都有一个 32x64 的 FIFO。⑤、片选信号 SS/CS时钟信号 SCLK 极性可配置。⑥、支持 DMA。2.2 ECSPI 时钟源的选择如下图所示时钟源来源于pll3_sw_clk并且经过了8 分频480/8 60 MHz设置CSCDR2的ECSPI_CLK_SEL选择pll3作为时钟源ECSPI_CLK_SEL0选择pll3_sw_clk并且经过8 分频作为时钟源1选择 OSC 的 24MHz 作为时钟源2.3 相关寄存器2.3.1 ECSPIx_CONREG 控制寄存器BURST_LENGTH数据长度位每次传输一字节的话就设置为7CHANNEL_SELECT通道选择位00通道001通道110通道211通道3PRE_DIVIDERSPI 预分频ECSPI 时钟频率使用两步来完成分频此位设置的是第一步可设置 0~15分别对应 1~16 分频注意PRE DIVIDER和POST_DIVIDER这两个分配器决定了SPI最终的时钟·由于默认情况下SPI的时钟来源于pll3_sw_clk的8分频即480M/860MHz·如果需要SPI时钟信号为1MHz·则可将POST_DIVIDER设置为4分频·60/415MHz,再将15M进行15分频,15MHz/151MHz。POST_DIVIDERSPI 分频值ECSPI 时钟频率的第二步分频设置分频值为 2^ POST_DIVIDERCHANNEL_MODESPI 通道主/从模式设置CHANNEL_MODE[3:0]分别对应 SP I 通道 3~0为 0 的话就是设置为从模式为 1 的话就是主模式比如设置为 0X01 的话就是设置通道 0 为主模式SMC开始模式控制此位只能在主模式下起作用为 0 的话通过 XCH 位来开启 SPI 突发访问为 1 的话只要向 TXFIFO 写入数据就开启 SPI 突发访问ENSPI 使能位为 0 的话关闭 SPI为 1 的话使能 SPI注意必须在操作其他寄存器前设置这一位2.3.2 ECSPIx_CONFIGREG 配置寄存器SCLK_CTL设置 SCLK 信号线空闲状态电平SCLK_CTL[3:0]分别对应通道 3 ~0为 0 的话 SCLK 空闲状态为低电平为 1 的话 SCLK 空闲状态为高电平SS_POL设置 SPI 片选信号极性设置SS_POL[3:0]分别对应通道 3~0为 0 的 话片选信号低电平有效为 1 的话片选信号高电平有效SCLK_POLSPI 时钟信号极性设置也就是 CPOLSCLK_POL[3:0]分别对应通 道 3~0为 0 的话 SCLK 高电平有效(空闲的时候为低电平)为 1 的话 SCLK 低电平有效(空闲 的时候为高电平)注意下面这两位要根据从机设备的要求来配置eg和ADXL345进行通信的话配置CPOL 1, CPHA 1SCLK_PHASPI 时钟相位设置也就是 CPHASCLK_PHA[3:0]分别对应通道 3 ~0为 0 的话串行时钟的第一个跳变沿(上升沿或下降沿)采集数据为 1 的话串行时钟的第二 个跳变沿(上升沿或下降沿)采集数据2.3.3 ECSPIx_STATREG 状态寄存器RRRXFIFO 就绪标志位为 0 的话 RXFIFO 没有数据为 1 的话表示 RXFIFO 中 至少有一个字的数据注意这一位判断是否接收到了数据while( (ECSPI3-STATREG (1 3)) 0 );TETXFIFO 空标志位为 0 表示 TXFIFO 中至少有一个字的数据为 1 表示 TXFI FO 为空注意这一位用于判断数据是否发送完毕while( (ECSPI3-STATREG (1 0)) 0 );三. IMX6ULL 使用 SPI 与ADXL345 通信3.1 ADXL345 简介ADXL345三轴数字加速度计3.1.1 主要参数工作电压2 v - 3.6 v量程±2g / ±4g / ±8g / ±16g分辨率±16g 时最高可达13bit灵敏度32 / (2^13 - 1) 32/8191 0.0039 3.9mg3.1.2 传感器读/写时序1、写时序可以看到1、在写入传感器时先向其发送了一字节的寄存器地址并且寄存器地址的最高位是 0 代表着写传感器同理最高位是 1 则代表读传感器2、也可以看到这款传感器的 SPI 时序在时钟线空闲时呈高电平并且是在时钟线的第二个边沿对数据进行采样这些都要在 SPI 初始化函数中要进行相应的配置2、读时序3.1.3 主要配置寄存器1、ID 寄存器0x00存放器件 ID固定为0xE52、输出速率设置寄存器 0x2C3、休眠设置寄存器0x2D4、中断设置寄存器0x2E设置为禁止中断5、分辨率设置寄存器设置为满分辨率13位6、数据传感器低字节数据存低地址高字节数据存高地址示例代码//原始数据 // x轴数据 ret.x adxl345_read(0x32); ret.x | adxl345_read(0x33) 8; // y轴数据 ret.y adxl345_read(0x34); ret.y | adxl345_read(0x35) 8; // z轴数据 ret.z adxl345_read(0x36); ret.z | adxl345_read(0x37) 8;3.2 SPI 初始化函数这里使用 SPI3注意1、将相应的引脚设置为相应的复用模式2、在设置引脚的电气属性时注意由于 SPI 是高速通信协议所以需要设置高速压摆率其余保持默认不变即可3、在设置寄存器 CONREG 的 EN 位时必须在操作其他寄存器前设置这一位故这一步应该放在最前面示例代码//初始化 SPI3 void spi3_init(void) { //1、引脚复用 IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0); //SPI3 片选线 IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0); //SPI3 时钟线 IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0); //SPI3 的MOSI IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0); //SPI3 的MISO //2、引脚电气属性 //注意这里要将 SRE 位置1因为SPI是高速通信需要设置高速压摆率其他不变 IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1); IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1); IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1); IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1); //3、设置片选GPIO1_IO20低电平片选故通信前先设置为高电平 GPIO1-GDIR | (1 20); GPIO1-DR | (1 20); //4、配置相关寄存器 //控制寄存器 ECSPI3-CONREG 0; ECSPI3-CONREG | (1 0); //使能 SPI ECSPI3-CONREG | (1 3); //向TXFIFO中写入数据立即启动一次通信 ECSPI3-CONREG | (1 4); //将通道0设置为主机模式 ECSPI3-CONREG | (2 8); //将Post Divider设置为4分频 ECSPI3-CONREG | (0x0E 12); //将Pre Divider设置为15分频 ECSPI3-CONREG | (7 20); //数据长度为8bit //ECSPI3-CONREG | (1 0); //使能 SPI //配置寄存器 ECSPI3-CONFIGREG 0; ECSPI3-CONFIGREG | (1 0); //设置通道0时钟相位为1在第二个跳变沿进行采样 ECSPI3-CONFIGREG | (1 4); //设置通道0时钟极性时钟线空闲时为高电平 ECSPI3-CONFIGREG | (1 20); //设置不工作时时钟线为高电平 //使能 SPI必须在操作其他寄存器前设置这一位 //ECSPI3-CONREG | (1 0); }3.3 SPI 读写函数SPI是全双工移位通信主从两侧各有一个移位寄存器。每个时钟沿到来时主机通过 MOSI 往外“移出”1位同时从 MISO “移入”1位。也就是——每“写”一位的同时必然“读”到一位。写即是读往总线“写”一个字节移出8位同一过程里也会“读”回从机在MISO上的8位可能是无效/占位字节。读即是写从从机“读”数据必须提供时钟所以主机必须同时“写”出一个占位字节通常是0xFF或0x00用这些时钟把从机的数据“移入”。示例代码使用标志位来判断读写是否完成//SPI3 读写函数---写即是读读即是写 unsigned int spi3_read_write(unsigned int data) { while( (ECSPI3-STATREG (1 0)) 0 ); ECSPI3-TXDATA data; while( (ECSPI3-STATREG (1 3)) 0 ); return (ECSPI3-RXDATA); }3.4 ADXL345 的读写函数注意1、读传感器时所发送的寄存器地址的最高位必须是 1spi3_read_write(reg_addr | 0x80);2、读传感器时必须发送一字节的占位数据3、读写前通过拉低片选线来进行选择传感器示例代码//ADXL345 写函数 void adxl345_write(unsigned char reg_addr, unsigned char data) { //1、拉低片选 GPIO1-DR ~(1 20); //2、发送寄存器地址 spi3_read_write(reg_addr); //3、发送数据 spi3_read_write(data); //4、拉高片选 GPIO1-DR | (1 20); } //ADXL345 读函数 unsigned char adxl345_read(unsigned char reg_addr) { unsigned char ret; //1、拉低片选 GPIO1-DR ~(1 20); //2、发送寄存器地址读时地址最高位是1 spi3_read_write(reg_addr | 0x80); //3、发送一个占位数据 ret spi3_read_write(0xFF); //4、拉高片选 GPIO1-DR | (1 20); return ret; }3.5 ADXL345 初始化函数前面介绍了 ADXL345 的一些寄存器初始化传感器就是对这些寄存器进行配置示例代码//ADXL345 初始化 void adxl345_init() { // 关闭中断 adxl345_write(0x2E, 0x08); // 满分辨率 13位 adxl345_write(0x31, 0x0B); // 速率 25HZ adxl345_write(0x2C, 0x08); // 工作模式 adxl345_write(0x2D, 0x0B); }3.6 读取传感器数据函数注意在转换原始数据时需要乘 0.0039示例代码://结构体定义 typedef struct ADXL345_Data { short x; short y; short z; float x_g; float y_g; float z_g; }ADXL345_Data_t; //读取原始数据 ADXL345_Data_t adxl345_get_vlalue(void) { ADXL345_Data_t ret; //原始数据 // x轴数据 ret.x adxl345_read(0x32); ret.x | adxl345_read(0x33) 8; // y轴数据 ret.y adxl345_read(0x34); ret.y | adxl345_read(0x35) 8; // z轴数据 ret.z adxl345_read(0x36); ret.z | adxl345_read(0x37) 8; //转换后数据 ret.x_g ret.x * 0.0039; ret.y_g ret.y * 0.0039; ret.z_g ret.z * 0.0039; return ret; }主函数调用//初始化 SPI3 spi3_init(); //初始化 ADXL345 adxl345_init(); ADXL345_Data_t ret { 0 }; unsigned char id 0; int i, j, k; while(1) { id adxl345_read(0); ret adxl345_get_vlalue(); printf(ADXL345_ID is %#x\n,id); printf(x:%d y:%d z:%d\n, ret.x, ret.y, ret.z); i ret.x_g * 10; if(i 0) { i * -1; printf(x_accel:-%d.%d ,i / 10, i % 10); } else { printf(x_accel:%d.%d ,i / 10, i % 10); } j ret.y_g * 10; if(j 0) { j * -1; printf(y_accel:-%d.%d ,j / 10, j % 10); } else { printf(y_accel:%d.%d ,j / 10, j % 10); } k ret.z_g * 10; if(k 0) { k * -1; printf(z_accel:-%d.%d\n,k / 10, k % 10); } else { printf(z_accel:%d.%d\n,k / 10, k % 10); } delay_ms(1000); } return 0;结果