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 CPHA
1 0:SCLK空闲为高电平,SPI下降沿采样
1 1:SCLK空闲为高电平,SPI上升沿采样
0 0:SCLK空闲为低电平,SPI上升沿采样
0 1:SCLK空闲为低电平,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 与 SDA
SPI 三或者四根线:SCLK,CS,MOSI,MISO
2、I2C通过七位从机地址对从机进行寻址
SPI通过将对应的片选电平拉低拉选择与哪个从机进行通信
3、I2C 标准速率 100K,高速为400K
SPI 速率可达M,几十或者几百MHz
4、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_SEL | 0:选择pll3_sw_clk并且经过8 分频作为时钟源 1:选择 OSC 的 24MHz 作为时钟源 |
2.3 相关寄存器
2.3.1 ECSPIx_CONREG 控制寄存器
| BURST_LENGTH | 数据长度位 每次传输一字节的话就设置为7 |
| CHANNEL_SELECT | 通道选择位 00:通道0 01:通道1 10:通道2 11:通道3 |
| PRE_DIVIDER | SPI 预分频 ECSPI 时钟频率使用两步来完成分频,此位设置的是第一步 可设置 0~15,分别对应 1~16 分频 注意:PRE DIVIDER和POST_DIVIDER这两个分配器决定了SPI最终的时钟·由于默认情况下SPI的时钟来源于pll3_sw_clk的8分频即480M/8=60MHz·如果需要SPI时钟信号为1MHz·则可将POST_DIVIDER设置为4分频·60/4=15MHz,再将15M进行15分频,15MHz/15=1MHz。 |
| POST_DIVIDER | SPI 分频值 ECSPI 时钟频率的第二步分频设置 分频值为 2^ POST_DIVIDER |
| CHANNEL_MODE | SPI 通道主/从模式设置 CHANNEL_MODE[3:0]分别对应 SP I 通道 3~0: 为 0 的话就是设置为从模式 为 1 的话就是主模式 比如设置为 0X01 的话就是设置通道 0 为主模式 |
| SMC | 开始模式控制,此位只能在主模式下起作用 为 0 的话通过 XCH 位来开启 SPI 突发访问, 为 1 的话只要向 TXFIFO 写入数据就开启 SPI 突发访问 |
| EN | SPI 使能位 为 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_POL | SPI 时钟信号极性设置,也就是 CPOL SCLK_POL[3:0]分别对应通 道 3~0 为 0 的话 SCLK 高电平有效(空闲的时候为低电平) 为 1 的话 SCLK 低电平有效(空闲 的时候为高电平) 注意:下面这两位要根据从机设备的要求来配置, eg:和ADXL345进行通信的话,配置:CPOL = 1, CPHA = 1 |
| SCLK_PHA | SPI 时钟相位设置,也就是 CPHA SCLK_PHA[3:0]分别对应通道 3 ~0 为 0 的话串行时钟的第一个跳变沿(上升沿或下降沿)采集数据 为 1 的话串行时钟的第二 个跳变沿(上升沿或下降沿)采集数据 |
2.3.3 ECSPIx_STATREG 状态寄存器
| RR | RXFIFO 就绪标志位 为 0 的话 RXFIFO 没有数据 为 1 的话表示 RXFIFO 中 至少有一个字的数据 注意:这一位判断是否接收到了数据 while( (ECSPI3->STATREG & (1 << 3)) == 0 ); |
| TE | TXFIFO 空标志位 为 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.9mg
3.1.2 传感器读/写时序
1、写时序
可以看到:
1、在写入传感器时,先向其发送了一字节的寄存器地址,并且寄存器地址的最高位是 0 代表着写传感器,同理,最高位是 1 则代表读传感器
2、也可以看到,这款传感器的 SPI 时序,在时钟线空闲时呈高电平,并且是在时钟线的第二个边沿对数据进行采样,这些都要在 SPI 初始化函数中要进行相应的配置
2、读时序
3.1.3 主要配置寄存器
1、ID 寄存器:0x00
存放器件 ID,固定为:0xE5
2、输出速率设置寄存器 0x2C
3、休眠设置寄存器:0x2D
4、中断设置寄存器: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、读传感器时,所发送的寄存器地址的最高位必须是 1
spi3_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;结果:
